using System;
using System.Collections.Generic;
using System.Reflection;
#if TEST
using Fadd.Tests;
using Xunit;
#endif

namespace Fadd
{
    /// <summary>
    /// General purpose class to get/set properties by specifying their name as text.
    /// </summary>
    /// <example>
    /// <code>
    /// User user = new User();
    /// Property.Set(user, "FirstName", "Jonas"); // same as user.FirstName = "Jonas";
    /// Property.Set(user, "CreatedAt", "2008-12-30 14:00"); // the CreatedAt property is of type DateTime, the Property class automatically converts it from string.
    /// Property.Set(user, "Age", "20"); // age is int, which means that it will automatically be converted.
    /// </code>
    /// </example>
    /// todo: Add the ability to extend the object with own additions.
    public static class Property
    {
        private const string Code =
			@"namespace Fadd {
public class FaddFTW : Fadd.IPropertiesObject
{
{PropertyTypes}

	public void Set(object item, string propertyName, object value)
	{
        Exception ex = null;
		{TypeName} obj = ({TypeName})item;
        try
        {
		switch (propertyName)
		{
{SetProperties}
            
            default:
                throw new ArgumentException(""Property '"" + propertyName + ""' is not found."");
		}
        }
        catch (InvalidCastException err)
        {
            ex = new InvalidCastException(""Failed to cast '"" + propertyName + ""' from '""  + value.GetType() + ""'"", err);
        }
		catch (FormatException err)
        {
            ex = new FormatException(""Failed to parse '"" + value.GetType() + ""' to '""  + propertyName + ""'"", err);
        }
        if (ex != null)
            throw ex;
	}

	public object Get(object item, string propertyName)
	{
		{TypeName} obj = ({TypeName})item;
		switch (propertyName)
		{
{GetProperties}
		}
        throw new ArgumentException(""Property '"" + propertyName + ""' is not found."");
	}
}}";
        private static readonly IDictionary<Type, IPropertiesObject> _assignerObjects = new Dictionary<Type, IPropertiesObject>();


        /// <summary>
        /// Assign a property value to the specified object.
        /// </summary>
        /// <param name="item">Object that has the property</param>
        /// <param name="propertyName">Name of the property</param>
        /// <param name="value">Value, this method will try to cast incorrect types (and even try to parse certain types).</param>
        /// <exception cref="ArgumentException">If value is not correct (i.e. null when null is not accepted etc)</exception>
        /// <exception cref="InvalidOperationException">If assigner object could not be generated.</exception>
        /// <exception cref="InvalidCastException">If assigner object can't cast to the correct type.</exception>
        /// <exception cref="ArgumentNullException">values is null and it's not allowed for the specified type.</exception>
        public static void Set(object item, string propertyName, object value)
        {
            Check.Require(item, "item");
            Check.NotEmpty(propertyName, "propertyName");

            IPropertiesObject obj = GetObject(item);
            if (obj == null)
                throw new InvalidOperationException("Failed to find object converter for " + item.GetType().FullName);

			try
			{
				obj.Set(item, propertyName, value);
			}
			catch (NullReferenceException err)
			{
				throw new ArgumentNullException(
					"Failed to assign '" + item.GetType().Name + "', property '" + propertyName + "'  with value " +
					(value == null ? "'" + value + "'" : "null"), err);
			}

        }

        /// <summary>
        /// Get a property value from the specified object.
        /// </summary>
        /// <param name="item">Object that has the property</param>
        /// <param name="propertyName">Name of the property</param>
        /// <returns>property value</returns>
        /// <exception cref="ArgumentException">If value is not correct (i.e. null when null is not accepted etc)</exception>
        /// <exception cref="InvalidOperationException">If assigner object could not be generated.</exception>
        /// <exception cref="InvalidCastException">If assigner object can't cast to the correct type.</exception>
        public static object Get(object item, string propertyName)
        {
            Check.Require(item, "item");
            Check.NotEmpty(propertyName, "propertyName");

            IPropertiesObject obj = GetObject(item);
            if (obj == null)
                throw new InvalidOperationException("Failed to find object converter for " + item.GetType().FullName);

            return obj.Get(item, propertyName);
        }

#if TEST
        [Fact]
        private static void TestAssigner()
        {
            PropertyTestObj1 obj = new PropertyTestObj1();
            PropertyTestObj2 obj2 = new PropertyTestObj2();
            Set(obj, "Age", "10");
            Set(obj, "CreatedAt", "2008-10-11 22:00");
            Set(obj, "FirstName", "jonas");
            Set(obj, "Obj2", obj2);
            Set(obj2, "Queue", new Queue()); // name clashes.

			// Test enumeration value
			Set(obj, "EnumerationValue", "Value2");
			Assert.Equal(PropertyTestObj1.TestEnumeration.Value2, obj.EnumerationValue);
        	Assert.Throws(typeof (ArgumentException), delegate { Set(obj, "EnumerationValue", "IncorrectValue!"); });
			
			// Test boolean values
			Set(obj, "BoolValue", "1");
			Assert.True(obj.BoolValue);
			Set(obj, "BoolValue", "0");
			Assert.False(obj.BoolValue);
			Set(obj, "BoolValue", "true");
			Assert.True(obj.BoolValue);
			Set(obj, "BoolValue", "false");
			Assert.False(obj.BoolValue);
			Set(obj, "Ratio", string.Empty);
			Assert.Equal(0.0, obj.Ratio);
			Assert.Throws(typeof(FormatException), delegate { Set(obj, "BoolValue", "Fsprak!"); });



            Assert.Throws(typeof (ArgumentException), delegate { Set(obj, "obj2", null); });
            Assert.Equal(10, (int)Get(obj, "Age"));
            Assert.Equal(new DateTime(2008, 10, 11, 22, 00, 00), (DateTime) Get(obj, "CreatedAt"));
            Assert.Equal("jonas", (string)Get(obj, "FirstName"));
            Assert.Throws(typeof(ArgumentException), delegate { Get(obj, "obj2"); });
            Assert.Equal(obj2, (PropertyTestObj2)Get(obj, "Obj2"));

        }

        [Fact]
        private static void TestEnumSet()
        {
            PropertyTestObj1 obj2 = new PropertyTestObj1();
            Property.Set(obj2, "EnumerationValue", string.Empty);
        }
#endif
        private static IPropertiesObject GetObject(object item)
        {
            IPropertiesObject obj;
            Type type = item.GetType();

            lock (_assignerObjects)
            {
                if (_assignerObjects.TryGetValue(type, out obj))
                    return obj;
            }

            Dictionary<Type, string> propertyTypes = new Dictionary<Type, string>();

            // generate set properties
            string sp = string.Empty;
			foreach (PropertyInfo info in type.GetProperties())
			{
				string propertyTypeName = Compiler.GetTypeName(info.PropertyType);
                if (!info.CanWrite || info.GetSetMethod(false) == null)
					continue;
                
                if (!propertyTypes.ContainsKey(info.PropertyType))
                    propertyTypes.Add(info.PropertyType, propertyTypeName.Replace("<", "").Replace(">", "").Replace(".", ""));

				sp += "			case \"" + info.Name + "\":\r\n";
				if (info.PropertyType.IsPrimitive)
					sp += "				if (value == null) break;";
				sp += "				if (value is " + propertyTypeName + ")\r\n";
				sp += "					obj." + info.Name + " = (" + propertyTypeName + ")value;\r\n";
				if (info.PropertyType == typeof(DateTime))
				{
					sp += "				else if (value is string)\r\n";
					sp += "					obj." + info.Name + " = DateTime.Parse((string)value);\r\n";
				}
				else if (info.PropertyType.IsEnum)
				{
					sp += "				else if (value is string)\r\n";
				    sp += "             {";
				    sp += "                 if (!string.IsNullOrEmpty((string)value))";
					sp += "					    obj." + info.Name + " = (" + propertyTypeName + ")Enum.Parse(typeof(" + propertyTypeName + "), (string)value, true);\r\n";
				    sp += "             }";
                    sp += "				else if (value is int)\r\n";
                    sp += "					obj." + info.Name + " = (" + propertyTypeName + ")(int)value;\r\n";
                }
				else if (info.PropertyType == typeof(bool))
				{
					sp += "				else if (value is string)\r\n";
					sp += "				{\r\n";
					sp += "					if(value == null)\r\n";
					sp += "						obj." + info.Name + " = false;\r\n";
					sp += "					if(((string)value).ToLower() == \"true\" || (string)value == \"1\")\r\n";
					sp += "						obj." + info.Name + " = true;\r\n";
					sp += "					else if(((string)value).ToLower() == \"false\" || (string)value == \"0\")\r\n";
					sp += "						obj." + info.Name + " = false;\r\n";
					sp += "					else\r\n";
					sp += "						throw new FormatException(\"Couldn't parse '\" + value + \"' into a boolean value\");\r\n";
					sp += "				}\r\n";
				}
				sp += "				else\r\n";
				sp += "				{\r\n";
				if (typeof(Convert).GetMethod("To" + info.PropertyType.Name, new[] { typeof(string) }) != null)
				{
					sp += "					if(value is string && string.IsNullOrEmpty((string)value))\r\n";
					sp += "						value = 0;\r\n";
				}
                sp += "					obj." + info.Name + " = (" + propertyTypeName + ")Convert.ChangeType(value, " + propertyTypes[info.PropertyType] + ");\r\n";
				sp += "				}\r\n";
				sp += "				break;\r\n";
			}

            // generate get properties.
            string gp = string.Empty;
            foreach (PropertyInfo info in type.GetProperties())
            {
                if (!info.CanRead || info.GetIndexParameters().Length > 0)
                    continue;

                gp += "			case \"" + info.Name + "\":\r\n";
                gp += "				return obj."+info.Name+";\r\n";
            }

            string propTypes = string.Empty;
            foreach (KeyValuePair<Type, string> pair in propertyTypes)
            {
                propTypes += "\tprivate static readonly Type " + pair.Value + " = typeof(" + Compiler.GetTypeName(pair.Key) + ");\r\n";
            }
                

            Compiler comp = new Compiler();
            comp.Add(item.GetType());
            comp.Add(typeof(IPropertiesObject));
            foreach (PropertyInfo info in type.GetProperties())
            {
                comp.Add(info.PropertyType);
                foreach (Type argument in info.PropertyType.GetGenericArguments())
                    comp.Add(argument);
            }

            string code = Code.Replace("{TypeName}", Compiler.GetTypeName(type));
            code = code.Replace("{GetProperties}", gp);
            code = code.Replace("{SetProperties}", sp);
            code = code.Replace("{PropertyTypes}", propTypes);
            comp.Compile(code);

            obj = comp.CreateInstance<IPropertiesObject>();
            lock (_assignerObjects)
            {
                if (!_assignerObjects.ContainsKey(type))
                    _assignerObjects.Add(type, obj);
            }

            return obj;
        }
    }

    /// <summary>
    /// Base object for property assigner.
    /// </summary>
    public interface IPropertiesObject
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propertyName"></param>
        /// <param name="value"></param>
        void Set(object item, string propertyName, object value);
        /// <summary>
        /// 
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        object Get(object item, string propertyName);
    }

    /// <summary>
    /// Used to determine 
    /// </summary>
    /// <param name="instance"></param>
    /// <param name="propertyName"></param>
    /// <returns></returns>
    public delegate bool CanSetHandler(object instance, string propertyName);

}
